import {
    CLASS_ACTIVE,
    CLASS_FADE,
    CLASS_FIXED,
    CLASS_FULLSCREEN_EXIT,
    CLASS_HIDE,
    CLASS_IN,
    CLASS_INVISIBLE,
    CLASS_LOADING,
    CLASS_SHOW,
    CLASS_TRANSITION,
    EVENT_CLICK,
    EVENT_HIDE,
    EVENT_LOAD,
    EVENT_SHOW,
    EVENT_TRANSITION_END,
    EVENT_VIEW,
    EVENT_VIEWED,
    EVENT_ZOOM,
    EVENT_ZOOMED,
    NAMESPACE,
} from './constants';
import {
    addClass,
    addListener,
    assign,
    dispatchEvent,
    escapeHTMLEntities,
    forEach,
    getData,
    getOffset,
    getPointersCenter,
    hasClass,
    isFunction,
    isNumber,
    isUndefined,
    removeClass,
    removeListener,
    setStyle,
    toggleClass,
} from './utilities';

export default {
    /** Show the viewer (only available in modal mode)
     * @param {boolean} [immediate=false] - Indicates if show the viewer immediately or not.
     * @returns {Viewer} this
     */
    show(immediate = false) {
        const {element, options} = this;

        if (options.inline || this.showing || this.isShown || this.showing) {
            return this;
        }

        if (!this.ready) {
            this.build();

            if (this.ready) {
                this.show(immediate);
            }

            return this;
        }

        if (isFunction(options.show)) {
            addListener(element, EVENT_SHOW, options.show, {
                once: true,
            });
        }

        if (dispatchEvent(element, EVENT_SHOW) === false || !this.ready) {
            return this;
        }

        if (this.hiding) {
            this.transitioning.abort();
        }

        this.showing = true;
        this.open();

        const {viewer} = this;

        removeClass(viewer, CLASS_HIDE);

        if (options.transition && !immediate) {
            const shown = this.shown.bind(this);

            this.transitioning = {
                abort: () => {
                    removeListener(viewer, EVENT_TRANSITION_END, shown);
                    removeClass(viewer, CLASS_IN);
                },
            };

            addClass(viewer, CLASS_TRANSITION);

            // Force reflow to enable CSS3 transition
            viewer.initialOffsetWidth = viewer.offsetWidth;
            addListener(viewer, EVENT_TRANSITION_END, shown, {
                once: true,
            });
            addClass(viewer, CLASS_IN);
        } else {
            addClass(viewer, CLASS_IN);
            this.shown();
        }

        return this;
    },

    /**
     * Hide the viewer (only available in modal mode)
     * @param {boolean} [immediate=false] - Indicates if hide the viewer immediately or not.
     * @returns {Viewer} this
     */
    hide(immediate = false) {
        const {element, options} = this;

        if (options.inline || this.hiding || !(this.isShown || this.showing)) {
            return this;
        }

        if (isFunction(options.hide)) {
            addListener(element, EVENT_HIDE, options.hide, {
                once: true,
            });
        }

        if (dispatchEvent(element, EVENT_HIDE) === false) {
            return this;
        }

        if (this.showing) {
            this.transitioning.abort();
        }

        this.hiding = true;

        if (this.played) {
            this.stop();
        } else if (this.viewing) {
            this.viewing.abort();
        }

        const {viewer} = this;

        if (options.transition && hasClass(this.image, CLASS_TRANSITION) && !immediate) {
            const hidden = this.hidden.bind(this);
            const hide = () => {
                // XXX: It seems the `event.stopPropagation()` method does not work here
                setTimeout(() => {
                    addListener(viewer, EVENT_TRANSITION_END, hidden, {
                        once: true,
                    });
                    removeClass(viewer, CLASS_IN);
                }, 0);
            };

            this.transitioning = {
                abort: () => {
                    if (this.viewed) {
                        removeListener(this.image, EVENT_TRANSITION_END, hide);
                    } else {
                        removeListener(viewer, EVENT_TRANSITION_END, hidden);
                    }
                },
            };

            // Note that the `CLASS_TRANSITION` class will be removed on pointer down (#255)
            if (this.viewed) {
                addListener(this.image, EVENT_TRANSITION_END, hide, {
                    once: true,
                });
                this.zoomTo(0, false, false, true);
            } else {
                hide();
            }
        } else {
            removeClass(viewer, CLASS_IN);
            this.hidden();
        }

        return this;
    },

    /**
     * View one of the images with image's index
     * @param {number} index - The index of the image to view.
     * @returns {Viewer} this
     */
    view(index = this.options.initialViewIndex) {
        index = Number(index) || 0;

        if (this.hiding || this.played || index < 0 || index >= this.length
            || (this.viewed && index === this.index)) {
            return this;
        }

        if (!this.isShown) {
            this.index = index;
            return this.show();
        }

        if (this.viewing) {
            this.viewing.abort();
        }

        const {
            element,
            options,
            title,
            canvas,
        } = this;
        const item = this.items[index];
        const img = item.querySelector('img');
        const url = getData(img, 'originalUrl');
        const alt = img.getAttribute('alt');
        const image = document.createElement('img');

        // forEach(options.inheritedAttributes, (name) => {
        //     const value = img.getAttribute(name);
        //
        //     if (value !== null) {
        //         image.setAttribute(name, value);
        //     }
        // });

        image.src = url;
        image.alt = alt;

        if (isFunction(options.view)) {
            addListener(element, EVENT_VIEW, options.view, {
                once: true,
            });
        }

        if (dispatchEvent(element, EVENT_VIEW, {
            originalImage: this.images[index],
            index,
            image,
        }) === false || !this.isShown || this.hiding || this.played) {
            return this;
        }

        this.image = image;
        removeClass(this.items[this.index], CLASS_ACTIVE);
        addClass(item, CLASS_ACTIVE);
        this.viewed = false;
        this.index = index;
        this.imageData = {};
        addClass(image, CLASS_INVISIBLE);

        if (options.loading) {
            addClass(canvas, CLASS_LOADING);
        }

        canvas.innerHTML = '';
        canvas.appendChild(image);

        // Center current item
        this.renderList();

        // Clear title
        title.innerHTML = '';

        // Generate title after viewed
        const onViewed = () => {
            const {imageData} = this;
            const render = Array.isArray(options.title) ? options.title[1] : options.title;

            title.innerHTML = escapeHTMLEntities(isFunction(render)
                ? render.call(this, image, imageData)
                : `${alt}`);
        };
        let onLoad;

        addListener(element, EVENT_VIEWED, onViewed, {
            once: true,
        });

        this.viewing = {
            abort: () => {
                removeListener(element, EVENT_VIEWED, onViewed);

                if (image.complete) {
                    if (this.imageRendering) {
                        this.imageRendering.abort();
                    } else if (this.imageInitializing) {
                        this.imageInitializing.abort();
                    }
                } else {
                    // Cancel download to save bandwidth.
                    image.src = '';
                    removeListener(image, EVENT_LOAD, onLoad);

                    if (this.timeout) {
                        clearTimeout(this.timeout);
                    }
                }
            },
        };

        if (image.complete) {
            this.load();
        } else {
            addListener(image, EVENT_LOAD, onLoad = this.load.bind(this), {
                once: true,
            });

            if (this.timeout) {
                clearTimeout(this.timeout);
            }

            // Make the image visible if it fails to load within 1s
            this.timeout = setTimeout(() => {
                removeClass(image, CLASS_INVISIBLE);
                this.timeout = false;
            }, 1000);
        }

        return this;
    },

    /**
     * View the previous image
     * @param {boolean} [loop=false] - Indicate if view the last one
     * when it is the first one at present.
     * @returns {Viewer} this
     */
    prev(loop = false) {
        let index = this.index - 1;

        if (index < 0) {
            index = loop ? this.length - 1 : 0;
        }

        this.view(index);
        return this;
    },

    /**
     * View the next image
     * @param {boolean} [loop=false] - Indicate if view the first one
     * when it is the last one at present.
     * @returns {Viewer} this
     */
    next(loop = false) {
        const maxIndex = this.length - 1;
        let index = this.index + 1;

        if (index > maxIndex) {
            index = loop ? 0 : maxIndex;
        }

        this.view(index);
        return this;
    },

    /**
     * Move the image with relative offsets.
     * @param {number} offsetX - The relative offset distance on the x-axis.
     * @param {number} offsetY - The relative offset distance on the y-axis.
     * @returns {Viewer} this
     */
    move(offsetX, offsetY) {
        const {imageData} = this;

        this.moveTo(
            isUndefined(offsetX) ? offsetX : imageData.left + Number(offsetX),
            isUndefined(offsetY) ? offsetY : imageData.top + Number(offsetY),
        );

        return this;
    },

    /**
     * Move the image to an absolute point.
     * @param {number} x - The x-axis coordinate.
     * @param {number} [y=x] - The y-axis coordinate.
     * @returns {Viewer} this
     */
    moveTo(x, y = x) {
        const {imageData} = this;

        x = Number(x);
        y = Number(y);

        if (this.viewed && !this.played && this.options.movable) {
            let changed = false;

            if (isNumber(x)) {
                imageData.left = x;
                changed = true;
            }

            if (isNumber(y)) {
                imageData.top = y;
                changed = true;
            }

            if (changed) {
                this.renderImage();
            }
        }

        return this;
    },

    /**
     * Zoom the image with a relative ratio.
     * @param {number} ratio - The target ratio.
     * @param {boolean} [hasTooltip=false] - Indicates if it has a tooltip or not.
     * @param {Event} [_originalEvent=null] - The original event if any.
     * @returns {Viewer} this
     */
    zoom(ratio, hasTooltip = false, _originalEvent = null) {
        const {imageData} = this;

        ratio = Number(ratio);

        if (ratio < 0) {
            ratio = 1 / (1 - ratio);
        } else {
            ratio = 1 + ratio;
        }

        this.zoomTo((imageData.width * ratio) / imageData.naturalWidth, hasTooltip, _originalEvent);

        return this;
    },

    /**
     * Zoom the image to an absolute ratio.
     * @param {number} ratio - The target ratio.
     * @param {boolean} [hasTooltip=false] - Indicates if it has a tooltip or not.
     * @param {Event} [_originalEvent=null] - The original event if any.
     * @param {Event} [_zoomable=false] - Indicates if the current zoom is available or not.
     * @returns {Viewer} this
     */
    zoomTo(ratio, hasTooltip = false, _originalEvent = null, _zoomable = false) {
        const {
            element,
            options,
            pointers,
            imageData,
        } = this;
        const {
            width,
            height,
            left,
            top,
            naturalWidth,
            naturalHeight,
        } = imageData;

        ratio = Math.max(0, ratio);

        if (isNumber(ratio) && this.viewed && !this.played && (_zoomable || options.zoomable)) {
            if (!_zoomable) {
                const minZoomRatio = Math.max(0.01, options.minZoomRatio);
                const maxZoomRatio = Math.min(100, options.maxZoomRatio);

                ratio = Math.min(Math.max(ratio, minZoomRatio), maxZoomRatio);
            }

            if (_originalEvent && ratio > 0.95 && ratio < 1.05) {
                ratio = 1;
            }

            const newWidth = naturalWidth * ratio;
            const newHeight = naturalHeight * ratio;
            const offsetWidth = newWidth - width;
            const offsetHeight = newHeight - height;
            const oldRatio = width / naturalWidth;

            if (isFunction(options.zoom)) {
                addListener(element, EVENT_ZOOM, options.zoom, {
                    once: true,
                });
            }

            if (dispatchEvent(element, EVENT_ZOOM, {
                ratio,
                oldRatio,
                originalEvent: _originalEvent,
            }) === false) {
                return this;
            }

            this.zooming = true;

            if (_originalEvent) {
                const offset = getOffset(this.viewer);
                const center = pointers && Object.keys(pointers).length ? getPointersCenter(pointers) : {
                    pageX: _originalEvent.pageX,
                    pageY: _originalEvent.pageY,
                };

                // Zoom from the triggering point of the event
                imageData.left -= offsetWidth * (((center.pageX - offset.left) - left) / width);
                imageData.top -= offsetHeight * (((center.pageY - offset.top) - top) / height);
            } else {
                // Zoom from the center of the image
                imageData.left -= offsetWidth / 2;
                imageData.top -= offsetHeight / 2;
            }

            imageData.width = newWidth;
            imageData.height = newHeight;
            imageData.ratio = ratio;
            this.renderImage(() => {
                this.zooming = false;

                if (isFunction(options.zoomed)) {
                    addListener(element, EVENT_ZOOMED, options.zoomed, {
                        once: true,
                    });
                }

                dispatchEvent(element, EVENT_ZOOMED, {
                    ratio,
                    oldRatio,
                    originalEvent: _originalEvent,
                });
            });

            if (hasTooltip) {
                this.tooltip();
            }
        }

        return this;
    },

    /**
     * Rotate the image with a relative degree.
     * @param {number} degree - The rotate degree.
     * @returns {Viewer} this
     */
    rotate(degree) {
        this.rotateTo((this.imageData.rotate || 0) + Number(degree));

        return this;
    },

    /**
     * Rotate the image to an absolute degree.
     * @param {number} degree - The rotate degree.
     * @returns {Viewer} this
     */
    rotateTo(degree) {
        const {imageData} = this;

        degree = Number(degree);

        if (isNumber(degree) && this.viewed && !this.played && this.options.rotatable) {
            imageData.rotate = degree;
            this.renderImage();
        }

        return this;
    },

    /**
     * Scale the image on the x-axis.
     * @param {number} scaleX - The scale ratio on the x-axis.
     * @returns {Viewer} this
     */
    scaleX(scaleX) {
        this.scale(scaleX, this.imageData.scaleY);

        return this;
    },

    /**
     * Scale the image on the y-axis.
     * @param {number} scaleY - The scale ratio on the y-axis.
     * @returns {Viewer} this
     */
    scaleY(scaleY) {
        this.scale(this.imageData.scaleX, scaleY);

        return this;
    },

    /**
     * Scale the image.
     * @param {number} scaleX - The scale ratio on the x-axis.
     * @param {number} [scaleY=scaleX] - The scale ratio on the y-axis.
     * @returns {Viewer} this
     */
    scale(scaleX, scaleY = scaleX) {
        const {imageData} = this;

        scaleX = Number(scaleX);
        scaleY = Number(scaleY);

        if (this.viewed && !this.played && this.options.scalable) {
            let changed = false;

            if (isNumber(scaleX)) {
                imageData.scaleX = scaleX;
                changed = true;
            }

            if (isNumber(scaleY)) {
                imageData.scaleY = scaleY;
                changed = true;
            }

            if (changed) {
                this.renderImage();
            }
        }

        return this;
    },

    /**
     * Play the images
     * @param {boolean} [fullscreen=false] - Indicate if request fullscreen or not.
     * @returns {Viewer} this
     */
    play(fullscreen = false) {
        if (!this.isShown || this.played) {
            return this;
        }

        const {options, player} = this;
        const onLoad = this.loadImage.bind(this);
        const list = [];
        let total = 0;
        let index = 0;

        this.played = true;
        this.onLoadWhenPlay = onLoad;

        if (fullscreen) {
            this.requestFullscreen();
        }

        addClass(player, CLASS_SHOW);
        forEach(this.items, (item, i) => {
            const img = item.querySelector('img');
            const image = document.createElement('img');

            image.src = getData(img, 'originalUrl');
            image.alt = img.getAttribute('alt');
            image.referrerPolicy = img.referrerPolicy;
            total += 1;
            addClass(image, CLASS_FADE);
            toggleClass(image, CLASS_TRANSITION, options.transition);

            if (hasClass(item, CLASS_ACTIVE)) {
                addClass(image, CLASS_IN);
                index = i;
            }

            list.push(image);
            addListener(image, EVENT_LOAD, onLoad, {
                once: true,
            });
            player.appendChild(image);
        });

        if (isNumber(options.interval) && options.interval > 0) {
            const play = () => {
                this.playing = setTimeout(() => {
                    removeClass(list[index], CLASS_IN);
                    index += 1;
                    index = index < total ? index : 0;
                    addClass(list[index], CLASS_IN);
                    play();
                }, options.interval);
            };

            if (total > 1) {
                play();
            }
        }

        return this;
    },

    // Stop play
    stop() {
        if (!this.played) {
            return this;
        }

        const {player} = this;

        this.played = false;
        clearTimeout(this.playing);
        forEach(player.getElementsByTagName('img'), (image) => {
            removeListener(image, EVENT_LOAD, this.onLoadWhenPlay);
        });
        removeClass(player, CLASS_SHOW);
        player.innerHTML = '';
        this.exitFullscreen();

        return this;
    },

    // Enter modal mode (only available in inline mode)
    full() {
        const {
            options,
            viewer,
            image,
            list,
        } = this;

        if (!this.isShown || this.played || this.fulled || !options.inline) {
            return this;
        }

        this.fulled = true;
        this.open();
        addClass(this.button, CLASS_FULLSCREEN_EXIT);

        if (options.transition) {
            removeClass(list, CLASS_TRANSITION);

            if (this.viewed) {
                removeClass(image, CLASS_TRANSITION);
            }
        }

        addClass(viewer, CLASS_FIXED);
        viewer.setAttribute('style', '');
        setStyle(viewer, {
            zIndex: options.zIndex,
        });

        this.initContainer();
        this.viewerData = assign({}, this.containerData);
        this.renderList();

        if (this.viewed) {
            this.initImage(() => {
                this.renderImage(() => {
                    if (options.transition) {
                        setTimeout(() => {
                            addClass(image, CLASS_TRANSITION);
                            addClass(list, CLASS_TRANSITION);
                        }, 0);
                    }
                });
            });
        }

        return this;
    },

    // Exit modal mode (only available in inline mode)
    exit() {
        const {
            options,
            viewer,
            image,
            list,
        } = this;

        if (!this.isShown || this.played || !this.fulled || !options.inline) {
            return this;
        }

        this.fulled = false;
        this.close();
        removeClass(this.button, CLASS_FULLSCREEN_EXIT);

        if (options.transition) {
            removeClass(list, CLASS_TRANSITION);

            if (this.viewed) {
                removeClass(image, CLASS_TRANSITION);
            }
        }

        removeClass(viewer, CLASS_FIXED);
        setStyle(viewer, {
            zIndex: options.zIndexInline,
        });

        this.viewerData = assign({}, this.parentData);
        this.renderViewer();
        this.renderList();

        if (this.viewed) {
            this.initImage(() => {
                this.renderImage(() => {
                    if (options.transition) {
                        setTimeout(() => {
                            addClass(image, CLASS_TRANSITION);
                            addClass(list, CLASS_TRANSITION);
                        }, 0);
                    }
                });
            });
        }

        return this;
    },

    // Show the current ratio of the image with percentage
    tooltip() {
        const {options, tooltipBox, imageData} = this;

        if (!this.viewed || this.played || !options.tooltip) {
            return this;
        }

        tooltipBox.textContent = `${Math.round(imageData.ratio * 100)}%`;

        if (!this.tooltipping) {
            if (options.transition) {
                if (this.fading) {
                    dispatchEvent(tooltipBox, EVENT_TRANSITION_END);
                }

                addClass(tooltipBox, CLASS_SHOW);
                addClass(tooltipBox, CLASS_FADE);
                addClass(tooltipBox, CLASS_TRANSITION);

                // Force reflow to enable CSS3 transition
                tooltipBox.initialOffsetWidth = tooltipBox.offsetWidth;
                addClass(tooltipBox, CLASS_IN);
            } else {
                addClass(tooltipBox, CLASS_SHOW);
            }
        } else {
            clearTimeout(this.tooltipping);
        }

        this.tooltipping = setTimeout(() => {
            if (options.transition) {
                addListener(tooltipBox, EVENT_TRANSITION_END, () => {
                    removeClass(tooltipBox, CLASS_SHOW);
                    removeClass(tooltipBox, CLASS_FADE);
                    removeClass(tooltipBox, CLASS_TRANSITION);
                    this.fading = false;
                }, {
                    once: true,
                });

                removeClass(tooltipBox, CLASS_IN);
                this.fading = true;
            } else {
                removeClass(tooltipBox, CLASS_SHOW);
            }

            this.tooltipping = false;
        }, 1000);

        return this;
    },

    // Toggle the image size between its natural size and initial size
    toggle() {
        if (this.imageData.ratio === 1) {
            this.zoomTo(this.initialImageData.ratio, true);
        } else {
            this.zoomTo(1, true);
        }

        return this;
    },

    // Reset the image to its initial state
    reset() {
        if (this.viewed && !this.played) {
            this.imageData = assign({}, this.initialImageData);
            this.renderImage();
        }

        return this;
    },

    // Update viewer when images changed
    update(images) {
        const {element, options} = this;

        if (!images.length) {
            return this;
        }

        this.images = images;
        this.length = images.length;

        if (this.ready) {
            const indexes = [];

            forEach(this.items, (item, i) => {
                const img = item.querySelector('img');
                const image = images[i];
                let url = typeof image === "object" ? (image || {}).url : image;
                if (image && img) {
                    if (url !== img.src) {
                        indexes.push(i);
                    }
                } else {
                    indexes.push(i);
                }
            });

            setStyle(this.list, {
                width: 'auto',
            });

            this.initList();

            if (this.isShown) {
                if (this.length) {
                    if (this.viewed) {
                        const index = indexes.indexOf(this.index);

                        if (index >= 0) {
                            this.viewed = false;
                            this.view(Math.max(this.index - (index + 1), 0));
                        } else {
                            addClass(this.items[this.index], CLASS_ACTIVE);
                        }
                    }
                } else {
                    this.image = null;
                    this.viewed = false;
                    this.index = 0;
                    this.imageData = {};
                    this.canvas.innerHTML = '';
                    this.title.innerHTML = '';
                }
            }
        } else {
            this.build();
        }

        return this;
    },

    // Destroy the viewer
    destroy() {
        const {element, options} = this;

        if (!element[NAMESPACE]) {
            return this;
        }

        this.destroyed = true;

        if (this.ready) {
            if (this.played) {
                this.stop();
            }
            if (this.isShown) {
                if (this.viewing) {
                    if (this.imageRendering) {
                        this.imageRendering.abort();
                    } else if (this.imageInitializing) {
                        this.imageInitializing.abort();
                    }
                }

                if (this.hiding) {
                    this.transitioning.abort();
                }

                this.hidden();
            } else if (this.showing) {
                this.transitioning.abort();
                this.hidden();
            }

            this.ready = false;
            this.viewer.parentNode.removeChild(this.viewer);
        }

        removeListener(element, EVENT_CLICK, this.onStart);

        element[NAMESPACE] = undefined;
        return this;
    },
};
